//
//  SketchDataRouter.java
//  SketchServer
//
//  Created by Brian Hinch on 9/4/08.
//  Copyright 2008 Tellart. All rights reserved.
//

/*
 * SketchDataRouter is a singleton class, use SketchDataRouter.getInstance() to get a reference
 *
 */
//import javax.comm.*;
import gnu.io.*;
//import Serialio.SerialPortLocal;

import java.io.*;
import java.util.TooManyListenersException;

public class SketchDataRouter implements SketchProtocolListener {

    private final static boolean TRACE_LOOP_STATE = false;
    
    private final static SketchDataRouter instance = new SketchDataRouter();
    private static final int IOPACK_SEND_DELAY = SketchProtocol.GET_RESPONSE_TIMEOUT_MS;
    private static final int IO_LOOP_DELAY = 1;
    
    private static final int IOPACK_WIRELESS_TIMEOUT = 1000;
    private static final int HANDHELD_WIRELESS_TIMEOUT = 1000;

    private boolean started = false;
    private IOPackSerialController ioPackController;
    private SketchSocketController handheldController;
	private SketchUpdateController sketchUpdateController;
    private MyThread myThread;
    
    private long lastIOPackActivityTimestamp;
    private long lastHandheldActivityTimestamp;

    private boolean messageFromIOPackIsWaiting;
    private int lastMessageFromIOPackLength;
    private byte[] lastMessageFromIOPack;
	
	private boolean autoSimulateIOPackData;
	private int autoSimulateIOPackDataCounter;
	private int autoSimulateIOPackDataCounterDirection = 1;
	
	private boolean handheldFlowControlEnabled;
	private boolean handheldFlowControlClearToSend;
    
    private boolean messageFromHandheldIsWaiting;
    private int lastMessageFromHandheldLength;
    private byte[] lastMessageFromHandheld;
    
    private byte[] preparedGetMessage;
    
    private byte[][]testSetMessages = new byte[3][SketchProtocol.SET_MESSAGE_LENGTH];
    private int testSetMessageIndex = 0;
    
    private static final int PROTOCOL_LISTENER_SOURCE_IOPACK = 1;
    private static final int PROTOCOL_LISTENER_SOURCE_HANDHELD = 2;
    
    public int[] monitorIOPackInputs;
    public int[] monitorIOPackOutputs;
    
    protected long skippedPolls = 0;
    
    private boolean pollIOPackController = false;

    private SketchDataRouter() {
        ioPackController = new IOPackSerialController(PROTOCOL_LISTENER_SOURCE_IOPACK, this);
        handheldController = new SketchSocketController(PROTOCOL_LISTENER_SOURCE_HANDHELD, this);
		sketchUpdateController = new SketchUpdateController();
        
        //prepare buffers to receive messages
        lastMessageFromIOPack = new byte[SketchProtocol.LONGEST_POSSIBLE_MESSAGE_LENGTH];
        lastMessageFromHandheld = new byte[SketchProtocol.LONGEST_POSSIBLE_MESSAGE_LENGTH];
        
        //prep a GET message that we'll resend over and over again to poll the IOPack
        preparedGetMessage = new byte[SketchProtocol.GET_MESSAGE_LENGTH];
        preparedGetMessage[SketchProtocol.MESSAGE_OFFSET_OPCODE] = SketchProtocol.GET_MESSAGE_OPCODE;
        
        monitorIOPackInputs = new int[SketchProtocol.SET_MESSAGE_NUM_VALUES];
        monitorIOPackOutputs = new int[SketchProtocol.SET_MESSAGE_NUM_VALUES];
        
        
    } //constructor
	
    public boolean getHandheldFlowControlEnabled() {
        return handheldFlowControlEnabled;
    }
    
	public void setHandheldFlowControlEnabled(boolean state) {
		handheldFlowControlEnabled = state;
	}
	
	public void setAutomaticSimulatedIOPackData(boolean state) {
		autoSimulateIOPackData = state;
	}
	
	public void setManualSimulatedIOPackData(int simValue) {
	
		//System.out.println("SketchDataRouter: setManualSimulatedIOPackData(" + v0 + ", " + v1 + ", " + v2 + ", " + v3 + ")");
	
		byte[] msg = new byte[SketchProtocol.SET_MESSAGE_LENGTH];
		msg[SketchProtocol.MESSAGE_OFFSET_OPCODE] = SketchProtocol.SET_MESSAGE_OPCODE;
		
        int[] pair;
        int bufferOffset;
        
        for (int i = 0; i < SketchProtocol.SET_MESSAGE_NUM_VALUES; i++) {
            bufferOffset = SketchProtocol.MESSAGE_OFFSET_OPCODE + SketchProtocol.MESSAGE_OPCODE_LENGTH + (i * SketchProtocol.MESSAGE_BYTES_PER_VALUE);
            pair = getMessageBytesForValue((i < (SketchProtocol.SET_MESSAGE_NUM_VALUES - 1)) ? simValue : Math.max(0, simValue - 1));
            msg[bufferOffset] = (byte)pair[0];
            msg[bufferOffset + 1] = (byte)pair[1];
        }
		lastMessageFromIOPack = msg;
        lastMessageFromIOPackLength = lastMessageFromIOPack.length;
        messageFromIOPackIsWaiting = true; //set the flag
		
		monitorIOPackInputs = parseMessage(lastMessageFromIOPack, 0, lastMessageFromIOPackLength);
		//update the activity timestamp
		lastIOPackActivityTimestamp = System.currentTimeMillis();
		
	} //setManualSimulatedIOPackData
    
    public static SketchDataRouter getInstance() {
        return SketchDataRouter.instance;
    }
    
    /********************************************************************/
    
    /*
     * Starts the router with a given configuration if the router has not already been started.
     * To change the configuration of the router it must first be stopped.
     *
     * @throws: AlreadyStartedException if the router has already been started
     */
    public void start(SketchDataRouterConfiguration config) throws UnsupportedCommOperationException, AlreadyStartedException, NoSuchPortException, TooManyListenersException, PortInUseException, IOException {
        if (started) throw new AlreadyStartedException();

        if (ioPackController.isStarted()) ioPackController.stop();
        if (handheldController.isStarted()) handheldController.stop();
		if (sketchUpdateController.isStarted()) sketchUpdateController.stop();
        
        ioPackController.start(config.getIOPackSerialPort());
        handheldController.start(config.getSketchServerNetworkInterface());
		sketchUpdateController.start(config.getSketchServerNetworkInterface());
        
        started = true;

        myThread = new MyThread();
        myThread.start();
        
    } //start
    
    /* Stops the router from running and closes down all internal resources  */
    public void stop() {
        if (!started) return;
    
		sketchUpdateController.stop();
        handheldController.stop();
        ioPackController.stop();
    
        started = false;

        try {
            myThread.interrupt();
        } catch (Exception e) {
            System.err.println("Exception: " + e.getLocalizedMessage());
            e.printStackTrace(System.err);
        }
    } //stop
    
    public boolean isStarted() {
        return started;
    } //isStarted
    
    public boolean getIOPackPollingEnabled() {
        return pollIOPackController;
    }
    
    public void setIOPackPollingEnabled(boolean state) {
        pollIOPackController = state;
        System.out.println("SketchDataRouter: IOPack polling set to "+state);
    }
    
    /*
     * from SketchProtocolListener
     *
     */
    public void onData(int source, byte[] data, int offset, int len) {
        //System.out.println("SketchDataRouter.onData: " + len + " bytes in from source " + source);
        
        if (len > SketchProtocol.LONGEST_POSSIBLE_MESSAGE_LENGTH) {
            //System.err.println("SketchDataRouter.onData: ***ERROR*** Incoming data is " + len + " bytes long, which exceeds the SketchProtocol.LONGEST_POSSIBLE_MESSAGE_LENGTH (" + SketchProtocol.LONGEST_POSSIBLE_MESSAGE_LENGTH + ")");
            return;
        }
        switch (source) {
            case PROTOCOL_LISTENER_SOURCE_IOPACK:
                //System.out.println("SketchDataRouter.onData: Filling lastMessageFromIOPack with incoming data");
                lastMessageFromIOPackLength = len;
                System.arraycopy(data, offset, lastMessageFromIOPack, 0, lastMessageFromIOPackLength);
                messageFromIOPackIsWaiting = true; //set the flag
                monitorIOPackInputs = parseMessage(lastMessageFromIOPack, 0, lastMessageFromIOPackLength);
                //update the activity timestamp
                lastIOPackActivityTimestamp = System.currentTimeMillis();
            break;
            case PROTOCOL_LISTENER_SOURCE_HANDHELD:
                System.out.println("SketchDataRouter.onData: Filling lastMessageFromHandheld with incoming data");
                lastMessageFromHandheldLength = len;
                System.arraycopy(data, offset, lastMessageFromHandheld, 0, lastMessageFromHandheldLength);
				if (handheldFlowControlEnabled && lastMessageFromHandheldIsGET()) {
					//handheld flow control is enabled, so we're waiting for handheld GETs
					//before sending any SETs from the IOPack. This message is a handheld GET
					//so turn on the clear to send flag to cause the most recent IOPack set to be sent to the handheld
					handheldFlowControlClearToSend = true;
					messageFromHandheldIsWaiting = false;
					
				} else {
					messageFromHandheldIsWaiting = true; //set the flag
				}
                //update the activity timestamp
                lastHandheldActivityTimestamp = System.currentTimeMillis();
            break;
        }
    } //onData
    
	private boolean lastMessageFromHandheldIsGET() {
		return (lastMessageFromHandheld != null) && (lastMessageFromHandheld.length >= SketchProtocol.MESSAGE_OFFSET_OPCODE) && (lastMessageFromHandheld[SketchProtocol.MESSAGE_OFFSET_OPCODE] == SketchProtocol.GET_MESSAGE_OPCODE);
	}
	
    public boolean isIOPackConnectionActive() {
        return ioPackController.isStarted() && ((System.currentTimeMillis() - lastIOPackActivityTimestamp) < IOPACK_WIRELESS_TIMEOUT);
    } //isIOPackWirelessActive
    
    public boolean isHandheldConnectionActive() {
        return handheldController.isStarted() && ((System.currentTimeMillis() - lastHandheldActivityTimestamp) < HANDHELD_WIRELESS_TIMEOUT);
    } //isHandheldWirelessActive
    
    public boolean isHandheldConnected() {
        return handheldController.isConnected();
    }
	
	
	private int[] getMessageBytesForValue(int value) {
		int[] r = new int[SketchProtocol.MESSAGE_BYTES_PER_VALUE];
		
		value = Math.max(0, Math.min(254, value));
		
		r[0] = (value >> 4) & 0x0F;
		r[1] = (value & 0x0F);
		
		return r;
		
	} //getMessageBytesForValue
	
    /*
     * parse a message and return the int values of its data
     *
     */
    private int[] parseMessage(byte[] data, int offset, int len) {
        //this array will hold the parsed int values (for the moment)
        int[] valuesTemp = new int[SketchProtocol.SET_MESSAGE_NUM_VALUES];
        
        //check the opcode
        if (data[SketchProtocol.MESSAGE_OFFSET_OPCODE] == SketchProtocol.SET_MESSAGE_OPCODE) {
            //convert encoded values
            int bufferOffset;

            //System.out.println("SketchDataRouter.parseMessage: --");
            for (int i = 0; i < valuesTemp.length; i++) {
                bufferOffset = SketchProtocol.MESSAGE_OFFSET_OPCODE + SketchProtocol.MESSAGE_OPCODE_LENGTH + (i * SketchProtocol.MESSAGE_BYTES_PER_VALUE);
                
                valuesTemp[i] = ((data[bufferOffset] & SketchProtocol.MESSAGE_VALUE_MASK) << 4)
                                +
                                (data[bufferOffset + 1] & SketchProtocol.MESSAGE_VALUE_MASK);
                
                //System.out.println("SketchDataRouter.parseMessage: value " + i +": " + valuesTemp[i]);
            }
            //System.out.println("SketchDataRouter.parseMessage: --");
            
            //done processing incoming message
            
        } else {
            System.err.println("SketchDataRouter.parseMessage: ERROR incoming message from IOPack had invalid opcode");
        }
        
        return valuesTemp;
        
    } //parseMessage

    /************************************************************************/
    
    private class MyThread extends Thread {
    
        /*
         * this is the thread run method
         *
         */
        public void run() {
			long t;
            while (started) {
				//t = System.currentTimeMillis();
				if (autoSimulateIOPackData) {
					autoSimulateIOPackDataCounter += autoSimulateIOPackDataCounterDirection;
					if (autoSimulateIOPackDataCounter + autoSimulateIOPackDataCounterDirection > 254) {
						autoSimulateIOPackDataCounterDirection = -1;
					} else if (autoSimulateIOPackDataCounter + autoSimulateIOPackDataCounterDirection < 0) {
						autoSimulateIOPackDataCounterDirection = 1;
					}
					setManualSimulatedIOPackData(autoSimulateIOPackDataCounter);
				}
			
                if (TRACE_LOOP_STATE) System.out.println("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ");
                if (TRACE_LOOP_STATE) System.out.println("SketchDataRouter.MyThread.run is looping...");
                
                if (handheldController.isReadyToSendMessages() && ((!handheldFlowControlEnabled && messageFromIOPackIsWaiting) || handheldFlowControlClearToSend) ) {
                    if (TRACE_LOOP_STATE) System.out.println("- - - SketchDataRouter.MyThread.run: STATE 1");
                    /* 
                     * State 1: there is a message to send from the IOPack to the handheld
                     * 
                     * iopack -> handheld
                     */
                    try {
                        if (TRACE_LOOP_STATE) System.out.println("SketchDataRouter.MyThread.run: About to call handheldController.sendMessage ...   ###############################");
                        if (handheldController.sendMessage(lastMessageFromIOPack, 0, lastMessageFromIOPackLength, 1, 0)) {
                            messageFromIOPackIsWaiting = false;
							handheldFlowControlClearToSend = false;
                            
                            //update the activity timestamp
                            lastHandheldActivityTimestamp = System.currentTimeMillis();

                        } //if send message was successful
                        
                    } catch (IOException ioe) {
						if (!ioe.getMessage().equals("Socket closed") && !ioe.getMessage().equals("Broken pipe")) {
							System.err.println("SketchDataRouter.MyThread.run: IOException: " + ioe.getMessage());
							ioe.printStackTrace(System.err);
						}
                    }

                } //end state 1
                
                if (ioPackController.isReadyToSendMessages() && messageFromHandheldIsWaiting) {
                    /*
                     * State 2: there is a message to send from the handheld to the IOPack
                     *
                     * handheld -> iopack
                     */
                    if (TRACE_LOOP_STATE) System.out.println("- - - SketchDataRouter.MyThread.run: STATE 2: going to send handheld -> iopack");
                    try {
                        if (ioPackController.sendMessage(lastMessageFromHandheld, 0, lastMessageFromHandheldLength, SketchProtocol.OUTGOING_SET_MESSAGE_RESEND_COUNT, SketchProtocol.OUTGOING_SET_MESSAGE_RESEND_DELAY)) {
                            messageFromHandheldIsWaiting = false;
							
                            //update the activity timestamp
                            //lastIOPackActivityTimestamp = System.currentTimeMillis();

							monitorIOPackOutputs = parseMessage(lastMessageFromHandheld, 0, lastMessageFromHandheldLength);
							
                            if (TRACE_LOOP_STATE) System.out.println("SketchDataRouter.MyThread.run: SENT. Pausing for " + IOPACK_SEND_DELAY + " ms");
                            Thread.sleep(IOPACK_SEND_DELAY);
                        
                        } //if sendMessage was successful
                        
                    } catch (InterruptedException ie) {
                    } catch (IOException ioe) {
                        System.err.println("SketchDataRouter.MyThread.run: IOException: " + ioe.getLocalizedMessage());
                        ioe.printStackTrace(System.err);
                    }
                
                } //end state 2
                
                if (pollIOPackController && ioPackController.isReadyToSendMessages()) {
                    /*
                     * State 3: nothing to pass between handheld and IOPack, so poll the IOPack for its current inputs
                     *
                     * router -> iopack
                     */
                    if (TRACE_LOOP_STATE) System.out.println("- - - SketchDataRouter.MyThread.run: STATE 3");
                    try {
                        if (ioPackController.sendMessage(preparedGetMessage, 0, preparedGetMessage.length, 1, 0)) {

                            //update the activity timestamp
                            //lastIOPackActivityTimestamp = System.currentTimeMillis();

                            if (TRACE_LOOP_STATE) System.out.println("SketchDataRouter.MyThread.run: SENT. Pausing for " + IOPACK_SEND_DELAY + " ms");
                            Thread.sleep(IOPACK_SEND_DELAY);
                            
                        } //if sendMessage was successful
                        
                    } catch (InterruptedException ie) {
                    } catch (IOException ioe) {
                        System.err.println("SketchDataRouter.MyThread.run: IOException: " + ioe.getLocalizedMessage());
                        ioe.printStackTrace(System.err);
                    }
                } else {
                    skippedPolls ++;
                    if (skippedPolls == Long.MAX_VALUE) skippedPolls = 0;
                    if (TRACE_LOOP_STATE) System.out.println("SketchDataRouter.MyThread.run: **** SKIPPED POLL **** skippedPolls=" + skippedPolls + " IOPACK_SEND_DELAY=" + IOPACK_SEND_DELAY + "ms IO_LOOP_DELAY=" + IO_LOOP_DELAY + "ms");
                } //end state 3
                
                try {
                    if (TRACE_LOOP_STATE) System.out.println("SketchDataRouter.MyThread.run: Pausing for " + IO_LOOP_DELAY + " ms");
                    Thread.sleep(IO_LOOP_DELAY);
                } catch (InterruptedException ie) { }
                
				//System.out.println("SketchDataRouter.MyThread.run -- loop took " + (System.currentTimeMillis() - t));
				
            } //while started
            
        } //run
    } //inner class

} //SketchDataRouter class
